/*
  Program: Mine Mayhem
  File:    board.cc
  Date:    July, 1996 to February,   1997,    version 1.00
	   26 October to 3 December,	      version 2.00
	   8 December to 4 January,  2001,    version 2.10
	   15 January,			      version 2.11
	   18 September to 16 December, 2006, version 2.90

  Everything to do with the board.

  Freeware, Copyright 2006 Jason Hood.
*/

#include "board.h"
#include "minefld.h"
#include "faces.h"
#include "mainmenu.h"
#include <tws/system.h>
#include <stdio.h>
#include <unistd.h>

extern bool  use_normal_timing;


#define ID_FACE 1000

DEFINE_RESPONSE_TABLE(FaceButton, BasicButton)
  E_RBUTTONDOWN
END_RESPONSE_TABLE


FaceButton::FaceButton(Window* parent, ControlID ID, int x, int y, int icon) :
  IconButton(parent, ID, x, y, x+FACEICONW-1, y+FACEICONH-1, FaceIcon[icon]),
  curicon(icon)
{
}

void FaceButton::ChangeIcon(int NewIcon)
{
  IconButton::ChangeIcon(FaceIcon[NewIcon]);
  curicon = NewIcon;
}

// Change the icon and display it as well.
void FaceButton::Display(int Icon)
{
  ChangeIcon(Icon);
  Paint();
  RefreshWindow();
}

// Display a different icon when the button is pressed.
void FaceButton::PaintWindow(int x1, int y1, int x2, int y2)
{
  IconButton::ChangeIcon(FaceIcon[isDown ? FACEPUSHED : curicon]);
  IconButton::PaintWindow(x1, y1, x2, y2);
}

void FaceButton::RButtonDown(int, int, int)
{
  ws.QueueEvent(parent, event(W_NOTIFY, butID, 1003));
}


DEFINE_RESPONSE_TABLE(BoardWindow, Window)
  E_BUTTONUP(ID_FACE, cmNewGame)
  if (!isPaused)
  {
    //E_LBUTTONDOWN
    //E_RBUTTONDOWN
    if (ev->Type == W_LBUTTONDOWN || ev->Type == W_RBUTTONDOWN)
    {
      ButtonDown(ev->p1, ev->p2, ev->Type == W_LBUTTONDOWN ? MBS_LEFT_BUTTON :
							     MBS_RIGHT_BUTTON);
      return TRUE;
    }
    //E_LBUTTONUP
    //E_RBUTTONUP
    if (ev->Type == W_LBUTTONUP || ev->Type == W_RBUTTONUP)
    {
      ButtonUp(ev->Type == W_LBUTTONDOWN ? MBS_LEFT_BUTTON : MBS_RIGHT_BUTTON);
      return TRUE;
    }
    E_MOUSEMOVE
    E_TIMER
  }
  E_BUTTONR(ID_FACE,	cmPause)
  E_KEYCODE(0x70, 0x19, cmPause);	// P
  E_KEYCODE(0x00, 0x86, cmDump);	// F12
  if (ev->Type == W_KEYPRESS && (ev->p1 >= '0' && ev->p1 <= '3'))
  {
    Main->cmHints(ev->p1 - '0');
    return TRUE;
  }

END_RESPONSE_TABLE


/* ----------------------------   BoardWindow	-----------------------------

  Create the board window.

*/

BoardWindow::BoardWindow() :
  Window(NULL, NULL, 0, 0, 0, 0, WA_VISABLE | WA_SAVEAREA | WA_BORDER),
  oldclues(0), olddim(0, 0, 0), isPaused(false)
{
  Face = new FaceButton(this, ID_FACE, FWIDTH+4, FWIDTH+stath-FACEICONH+1,
			      FACESMILEY);
}


BoardWindow::~BoardWindow()
{
  delete Face;
}


/* -------------------------------   Resize   -------------------------------

  Set the board's size and position according to the dimensions.

*/

void BoardWindow::Resize(int width, int height)
{
  // Ensure there's enough room for the overall efficiency.
  int eff = StringWidth("100%", SysFont);
  if (FACEICONW + 2*(3*DIGITW+2+1) + 2*eff > width)
    width = FACEICONW + 2*(3*DIGITW+2+1) + 2*eff;

  //	    grid     frame   margin outline
  int wid = width + FWIDTH*2 + 4*2 + 2;

  //	     <------------ menu ---------->  margin
  int ypos = SysFont->height + FWIDTH*2 + 4 + 4;

  Face->ShiftX((wid-FACEICONW)/2 - Face->x1);	// Re-centre the face button
  Window::Resize((ws.GetDeskWidth()-wid)/2, ypos, (ws.GetDeskWidth()+wid)/2-1,
  //		margin outline	grid	 frame
  ypos + stath + 4*2 +	2   + height + FWIDTH*2 - 1);
}


/* ----------------------------   PaintWindow	-----------------------------

  Draw the outline around the grid, the frames around the counts and time, and
  display the dimensions and difficulty.  Calculate some display offsets.

*/

void BoardWindow::PaintWindow(int x1, int y1, int x2, int y2)
{
  Window::PaintWindow(x1, y1, x2, y2);

  ViewBuffer* buf = GetSubBuffer(x1, y1, x2, y2);

  // Top-left co-ordinate of the grid itself
  ox	  = cx1 + (cx2 - cx1 + 1 - olddim.dim.x) / 2;
  oy	  = cy1 + Face->y2 + 4;
  spacesx = cx2 - 5 - 3*DIGITW; 	// Offset for space count
  timex   = Face->x2 - 3*DIGITW - 1;	// Offset for time, above the face
					//  button, which is already centred

  // Draw the "shadow box" (Coyote's term) around the counts and time.
  // This does actually look better than version 1.
  #define F4 FWIDTH+4
  #define DW 3*DIGITW
  #define DH DIGITH
  int dyb = Face->y2, dyt = dyb - DIGITH - 2 + 1;
  InvFrame(buf, F4-1, dyt, F4+DW, dyb);
  InvFrame(buf, FWIDTH+spacesx-1, dyt, FWIDTH+spacesx+DW, dyb);
  InvFrame(buf, FWIDTH+timex-1, F4-1, FWIDTH+timex+DW, F4+DH);
  InvFrame(buf, FWIDTH+timex+DW, F4+DH-SMALLDIGITH-1,
		FWIDTH+timex+DW+3*SMALLDIGITW, F4+DH);
  Pixel(buf, FWIDTH+timex+DW, F4+DH, 17);
  #undef F4
  #undef DW
  #undef DH

  field->Outline(buf, ox-1, oy-1, ox+olddim.dim.x, oy+olddim.dim.y);

  DeleteBuffer(buf);
}


/* -------------------------------   NewGame   ------------------------------

  Start a new game with the given dimensions.  If the dimensions are not the
  same as the previous game resize the window.

*/

void BoardWindow::NewGame(cCoord& dim, int mines, int spaces)
{
  ws.StopTimer(this);
  if (isPaused)
  {
    isPaused = false;
    DeleteBuffer(gridsave);
  }

  if (!(olddim.dim == dim))
  {
    olddim.dim = dim;
    olddim.mines = mines;
    Resize(dim.x, dim.y);
  }
  else if (olddim.mines != mines)
  {
    olddim.mines = mines;
    Paint();
  }

  DisplayFace();
  secs = mils = 0;
  DisplayTime(true);
  progress = true;
  oldmines = oldspaces = -1;
  DisplayProgress(mines, spaces);
  DisplayStats(false);
  DisplayClues(-1);

  clicks[0]	= 1;		// the first open happens prior to playing
  clicks[1]	=
  goodclicks[0] =
  goodclicks[1] = 0;

  old_t = NULL;
  butt	= 0;
}


// Start a new game from the face button.
void BoardWindow::cmNewGame()
{
  field->NewGame(field->level);
}


/* ----------------------------   StartTiming	-----------------------------

  Initialise the time calibration and start the timer.

*/

void BoardWindow::StartTiming()
{
  oldsecs   = 0;
  startmils = (use_normal_timing) ? GetTicks() : uclock();
  ws.StartTimer(this, 250);
}


/* -------------------------------   Timer   --------------------------------

  The timer routine, called every quarter-second.  It reads the system time
  and updates the display if the seconds have changed.

*/

void BoardWindow::Timer()
{
  if (isFocus())
  {
    if (use_normal_timing)
      secs = (GetTicks() - startmils) / 1000;
    else
      secs = (uclock() - startmils) / UCLOCKS_PER_SEC;
    if (secs != oldsecs)
    {
      oldsecs = secs;
      DisplayTime();
    }
  }
}


/* ------------------------------   GameOver   ------------------------------

  Draw the appropriate face, display the final time (ie. including the milli-
  seconds) and return the time taken (in milliseconds).

*/

int BoardWindow::GameOver(bool dead)
{
  ws.StopTimer(this);
  int score;
  if (use_normal_timing)
    score = GetTicks() - startmils;
  else
    score = (uclock() - startmils) / (UCLOCKS_PER_SEC / 1000);
  if (score < 1)
    score = 1;
  else if (score > 999999)
    score = 999999;
  secs = score / 1000;
  mils = score % 1000;
  DisplayTime(true);
  Face->Display((dead) ? FACEDEAD : FACECOOL);
  progress = dead;

  ticks = GetTicks();

  return score;
}


/* ----------------------------   DisplayTime   -----------------------------

  Display the time.  If full is true it displays the milliseconds, otherwise
  only the seconds are displayed.

*/

void BoardWindow::DisplayTime(bool full)
{
  Display(secs, timex, 4);
  if (full)
    Display(mils, timex+3*DIGITW, 4+DIGITH-SMALLDIGITH, SmallDigit, true);
}


/* -----------------------------   DisplayFace	 ----------------------------

  Choose which face to display, depending on records being kept.

*/

void BoardWindow::DisplayFace()
{
  int p = field->GameProgress();
  if (p == FINISHED)
    return;
  Face->Display((Game == CROSS || field->isRestarted ||
		 (isAutoMark && isAutoOpen) || field->level < BEGINNER ||
		 (field->level == CUSTOM
		  && field->CustomPopup.Find(field->layout) < 0) ||
		 (p == NOT_PLAYING && Hints > CLUES) ||
		 (p == PLAYING && hintsused > CLUES))
		? FACEBLAND : FACESMILEY);
}


/* ---------------------------	 DisplayProgress   ---------------------------

  Display the number of mines marked and/or the number of spaces to uncover.
  If playing the clear game, use the millisecond time display to indicate the
  percentage completed; the cross game displays the distance from the end.

*/

void BoardWindow::DisplayProgress(int mines, int spaces)
{
  if (mines != oldmines)
  {
    Display(mines, 4, Face->y2 - DIGITH - cy1);
    oldmines = mines;
  }
  if (spaces != oldspaces)
  {
    Display(spaces, spacesx, Face->y2 - DIGITH - cy1);
    oldspaces = spaces;
  }

  if (progress)
    Display(field->Progress(), timex+3*DIGITW,4+DIGITH-SMALLDIGITH, SmallDigit);
}


/* ----------------------------   DisplayClues	 -----------------------------

  Display the number of known tiles to the left of the smiley.

*/

void BoardWindow::DisplayClues(int clues)
{
  if (clues == oldclues)
    return;

  int xlo, xln, xr = Face->x1 - 3,
      yb = Face->y2,
      yt = yb - SysFont->height + 1;

  char temp[8];
  sprintf(temp, "%d", oldclues);
  xlo = xr - StringWidth(temp, SysFont);
  Box(wnd, xlo, yt, xr, yb, 14);
  oldclues = clues;
  if (clues != -1)
  {
    sprintf(temp, "%d", oldclues);
    xln = xr - StringWidth(temp, SysFont);
    WriteText(wnd, xln, yt, 0, SysFont, temp);
  }
  else
    xln = xlo;
  RefreshWindow(absx+MIN(xln, xlo), absy+yt, absx+xr, absy+yb);
}


/* ----------------------------   DisplayStats	 -----------------------------

  Display the "before" or "after" game statistics.  The before stats are the
  layout and difficulty.  The after stats are the 3BV, 3BV/s and clicking
  efficiency.  Efficiency is the percentage of "good" clicks to total clicks.
  A good click is one which opens a tile or marks a mine (unmarking a mine will
  remove a good click).  Tracking will only count the initial click, not any
  subsequent actions.

*/

void BoardWindow::DisplayStats(bool after)
{
  int xl  = cx1+4,			// layout, 3BV, right clicks (left edge)
      xr  = cx2-4,			// diff, 3BV/s, left clicks (right edge)
      xm1 = Face->x2+3, 		// overall clicks (left edge)
      xm2 = spacesx-2;			// overall clicks (right edge)
  int yt  = cy1,			// layout (top edge)
      ye  = yt+SysFont->height, 	// left/right clicks (top edge)
      yb  = ye+SysFont->height-1,	// left/right clicks (bottom edge)
      ym2 = Face->y2,			// overall clicks (bottom edge)
      ym1 = ym2-SysFont->height+1;	// overall clicks (top edge)

  // Blank out all the previous stats.
  Box(wnd, xl, yt, timex-3, yb, 14);
  Box(wnd, timex+3*DIGITW+3*SMALLDIGITW+3, yt, xr, yb, 14);
  Box(wnd, xm1, ym1, xm2, ym2, 14);

  char s1[16], s2[8];
  if (after)
  {
    if (Game == CROSS)
      s1[0] = s2[0] = '\0';
    else
    {
      int f3bv = field->Find3BV();
      int len = sprintf(s1, "%d", f3bv);
      if (!field->GameWon())
	f3bv = field->Find3BV(true);
      if (f3bv) 		// First click on a mine has a 3BV of 0
      {
	sprintf(s1+len, " (%.2f)",
		double(field->Opened() + field->Marked()) / f3bv);
	if (xl+StringWidth(s1, SysFont) > timex-3)
	  s1[len] = '\0';
	double f3bvps = f3bv / (secs + mils / 1000.);
	// An exceptionally quick game (such as with both autos on) may result
	// in an abnormally large value, so adjust decimals to fit it within
	// a smaller board size.
	sprintf(s2, (f3bvps < 10) ? "%.3f" : (f3bvps < 100) ? "%.2f" : "%.0f",
		f3bvps);
      }
      else
	s2[0] = '\0';
    }

    char pc[8];
    if (clicks[1] != 0)
    {
      // Display right-click efficiency above the mines counter,
      sprintf(pc, "%d%%", 100 * goodclicks[1] / clicks[1]);
      WriteText(wnd, xl, ye, 0, SysFont, pc);
      // the left-click efficiency above the spaces counter,
      sprintf(pc, "%d%%", 100 * goodclicks[0] / clicks[0]);
      WriteText(wnd, xr-StringWidth(pc, SysFont)+1, ye, 0, SysFont, pc);
    }
    // and the combined efficiency to the right of the smiley button.
    sprintf(pc, "%d%%", 100 * (goodclicks[0]+goodclicks[1])
				/ (clicks[0]+clicks[1]));
    WriteText(wnd, xm1, ym1, 0, SysFont, pc);
  }
  else
  {
    sprintf(s1, "%d x %d", field->layout.dim.x, field->layout.dim.y);
    sprintf(s2, "%.3g", field->layout.diff());
  }

  // Display the dimensions or 3BV in the top-left corner ...
  WriteText(wnd, xl, yt, 0, SysFont, s1);
  // ... and the difficulty or 3BV/s in the top-right
  WriteText(wnd, xr-StringWidth(s2, SysFont)+1, yt, 0, SysFont, s2);

  RefreshWindow(absx+xl, absy+yt, absx+xr, absy+ym2);
}


/* ------------------------------   Display   -------------------------------

  Display the number num at (x, y) (relative to the board window's client
  area).  It uses the bmp[] buffers for each digit (0 to 9, 10 for minus, 11
  for blank, all the same dimensions).  If zeros is true the number is padded
  with zeros, otherwise blanks.

*/

void BoardWindow::Display(int num, int x, int y, ViewBuffer* bmp[], bool zeros)
{
  char str[4];
  int n, x1, j,
      wid = bmp[0]->width, hyt = bmp[0]->height;

  x += cx1;
  y += cy1;

  if (num > 999)
    num = 999;
  else if (num < -99)
    num = -99;
  sprintf(str, (zeros) ? "%03d" : "%3d", num);
  for (x1 = x, j = 0; j < 3; x1 += wid, j++)
  {
    n = (str[j] == ' ') ? EMPTY : (str[j] == '-') ? MINUS : (str[j] - '0');
    BitBlt(wnd, x1, y, bmp[n], 0, 0, wid-1, hyt-1);
  }
  RefreshWindow(absx+x, absy+y, absx+x1-1, absy+y+hyt-1);
}


/* ---------------------------	 DisplayTile   ----------------------------

  Paint the tile onto the board.

*/

void BoardWindow::DisplayTile(int x, int y, int w, int h, ViewBuffer* tile)
{
  x += ox;
  y += oy;
  BitBltTransparent(wnd, x, y, tile, 0, 0, w, h);

  if (x   < rx1) rx1 = x;
  if (x+w > rx2) rx2 = x+w;
  if (y   < ry1) ry1 = y;
  if (y+h > ry2) ry2 = y+h;
}


/* ------------------------------   Display   -------------------------------

  Refresh the changed tiles of the field onto the screen.

*/

void BoardWindow::Display()
{
  if (rx2 != -1)
  {
    RefreshWindow(absx+rx1, absy+ry1, absx+rx2, absy+ry2);
    rx1 = ry1 = x2;
    rx2 = ry2 = -1;
  }
}


/* -----------------------------   ButtonDown	-----------------------------

  Normalise the co-ordinate and pass it along to the field.  If the game is
  finished right-click will start a new one (after a slight delay to prevent
  an unneeded mark).

*/

void BoardWindow::ButtonDown(int x, int y, int b)
{
  int p = field->GameProgress();
  if (p == FINISHED)
  {
    if (b == MBS_RIGHT_BUTTON && GetTicks() - ticks >= 250)
      field->NewGame();
    return;
  }

  if (p == PLAYING)
    ++clicks[b >> 1];

  x -= absx + ox;
  y -= absy + oy;
  if (field->Convert(x, y, old_t))
  {
    butt |= b;
    int good = (b == MBS_LEFT_BUTTON) ? field->LeftButton(old_t)
				      : field->RightButton(old_t);
    p = field->GameProgress();
    if (p == NOT_PLAYING)	// grid generation was cancelled
      return;

    goodclicks[b >> 1] += good;
    if (p == FINISHED)
    {
      if (Game == CLEAR && field->GameWon())
	DisplayProgress(0, 0);
      else
	DisplayProgress(field->FlagsLeft(), field->SpacesLeft());
      DisplayStats(true);
      Display();
      if (Game == CLEAR && !field->isRestarted && hintsused < WHERE)
	field->times.Check(field->level, field->Find3BV(), field->score);
    }
    else
    {
      DisplayProgress(field->FlagsLeft(), field->SpacesLeft());
      if (good)
	field->Solve();
      Display();
    }
  }
}


/* ------------------------------   ButtonUp   ------------------------------

  Remove the highlight, if present.

*/

void BoardWindow::ButtonUp(int b)
{
  field->UnHighlight();
  Display();
  butt &= ~b;
}


/* -----------------------------   MouseMove   ------------------------------

  If tracking is on, follow the mouse.	Only track if one button is pressed.

*/

void BoardWindow::MouseMove(int x, int y, int, int but_stat)
{
  Tile* t;

  if (!isTracked || butt == 0)
    return;

  x -= absx + ox;
  y -= absy + oy;
  if (field->Convert(x, y, t) && t != old_t)
  {
    switch (but_stat)
    {
      case MBS_LEFT_BUTTON:
	field->UnHighlight();
	field->LeftButton(t);
      break;
      case MBS_RIGHT_BUTTON:
	field->UnHighlight();
	field->RightButton(t);
      break;
    }
    old_t = t;
  }
}


/* ------------------------------   cmPause   -------------------------------

  Pause the game.  Display a sleepy face and blank the board.  Call again to
  unpause.  If the game has finished restart it.

*/


void BoardWindow::cmPause(int)
{
  int p = field->GameProgress();
  if (p == FINISHED)
  {
    field->NewGame(1);
    return;
  }
  else if (p == NOT_PLAYING)
    return;

  Coord dim = field->GetBoardDimensions(field->layout.dim);
  int wid = dim.x, hyt = dim.y;

  if (isPaused)
  {
    isPaused = false;
    DisplayFace();
    BitBlt(wnd, ox, oy, gridsave, 0, 0, wid-1, hyt-1);
    DeleteBuffer(gridsave);
    field->ShowHints();
    RefreshWindow(absx+ox, absy+oy, absx+ox+wid-1, absy+oy+hyt-1);
    startmils += ((use_normal_timing) ? GetTicks() : uclock()) - pausemils;
  }
  else
  {
    isPaused = true;
    pausemils = (use_normal_timing) ? GetTicks() : uclock();
    gridsave = NewBuffer(wid, hyt);
    BitBlt(gridsave, 0, 0, wnd, ox, oy, ox+wid-1, oy+hyt-1);
    Face->Display(FACESLEEPY);
    field->Pause();
    RefreshWindow(absx+ox, absy+oy, absx+ox+wid-1, absy+oy+hyt-1);
  }
}


/* -------------------------------   cmDump   --------------------------------

  Save the board as a bitmap.  Complicated by the fact that TWS will only save
  24-bit colour bitmaps and there is no get pixel routine (I'll assume the
  config remains 8-bit).

*/

void BoardWindow::cmDump(int)
{
  // Create the first 20 system colours.
  unsigned char rgb[20*3];
  int r, g, b, c;
  sscanf(ws.GetConfigItem("system_colour"), "%d,%d,%d", &r, &g, &b);
  c = 3;
  rgb[0] = rgb[1] = rgb[2] = 0;
  for (int j = 1; j < 20; ++j)
  {
    rgb[c++] = r * j / 19;
    rgb[c++] = g * j / 19;
    rgb[c++] = b * j / 19;
  }

  // Translate the 8-bit buffer to a 24-bit buffer.  BitBlt should do this,
  // but doesn't.
  ViewBuffer* buf = NewBuffer24(wnd->width, wnd->height);
  unsigned char* pos = (unsigned char*)wnd->base;
  for (int y = 0; y < wnd->height; ++y)
  {
    for (int x = 0; x < wnd->width; ++x)
    {
      c = *pos++;
      if (c < 20)
      {
	c *= 3;
	r = rgb[c];
	g = rgb[c+1];
	b = rgb[c+2];
      }
      else // if (c <= 235)	// Custom colours aren't used
      {
	c -= 20;
	r = c / 36 * 51; c %= 36;
	g = c / 6  * 51;
	b = c % 6  * 51;
      }
      RGBPixel(buf, x, y, r, g, b);
    }
  }

  // Find the first available filename: mine0001.bmp, mine0002.bmp, ...
  char name[16];
  for (int num = 1; ; ++num)
  {
    sprintf(name, "mine%04d.bmp", num);
    if (access(name, F_OK))
      break;
  }
  FILE* dump = fopen(name, "wb");
  SaveBMP(buf, dump);
  fclose(dump);

  DeleteBuffer(buf);
}
